// Take a look at the license at the top of the repository in the LICENSE file.

use std::collections::{HashMap, hash_map};

use crate::network::refresh_networks_addresses;
use crate::{IpNetwork, MacAddr, NetworkData};

macro_rules! old_and_new {
    ($ty_:expr, $name:ident, $old:ident, $data:expr) => {{
        $ty_.$old = $ty_.$name;
        $ty_.$name = $data.$name;
    }};
}

pub(crate) struct NetworksInner {
    pub(crate) interfaces: HashMap<String, NetworkData>,
}

impl NetworksInner {
    pub(crate) fn new() -> Self {
        Self {
            interfaces: HashMap::new(),
        }
    }

    pub(crate) fn list(&self) -> &HashMap<String, NetworkData> {
        &self.interfaces
    }

    pub(crate) fn refresh(&mut self, remove_not_listed_interfaces: bool) {
        unsafe {
            self.refresh_interfaces(true);
        }
        if remove_not_listed_interfaces {
            // Remove interfaces which are gone.
            self.interfaces.retain(|_, i| {
                if !i.inner.updated {
                    return false;
                }
                i.inner.updated = false;
                true
            });
        }
        refresh_networks_addresses(&mut self.interfaces);
    }

    unsafe fn refresh_interfaces(&mut self, refresh_all: bool) {
        unsafe {
            let Some(ifaddrs) = InterfaceAddressIterator::new() else {
                sysinfo_debug!("getifaddrs failed");
                return;
            };

            for ifa in ifaddrs {
                let ifa = &*ifa;
                if let Some(name) = std::ffi::CStr::from_ptr(ifa.ifa_name)
                    .to_str()
                    .ok()
                    .map(|s| s.to_string())
                {
                    let data: &libc::if_data = &*(ifa.ifa_data as *mut libc::if_data);
                    let mtu = data.ifi_mtu;
                    match self.interfaces.entry(name) {
                        hash_map::Entry::Occupied(mut e) => {
                            let interface = e.get_mut();
                            let interface = &mut interface.inner;

                            old_and_new!(interface, ifi_ibytes, old_ifi_ibytes, data);
                            old_and_new!(interface, ifi_obytes, old_ifi_obytes, data);
                            old_and_new!(interface, ifi_ipackets, old_ifi_ipackets, data);
                            old_and_new!(interface, ifi_opackets, old_ifi_opackets, data);
                            old_and_new!(interface, ifi_ierrors, old_ifi_ierrors, data);
                            old_and_new!(interface, ifi_oerrors, old_ifi_oerrors, data);
                            if interface.mtu != mtu {
                                interface.mtu = mtu;
                            }
                            interface.updated = true;
                        }
                        hash_map::Entry::Vacant(e) => {
                            if !refresh_all {
                                // This is simply a refresh, we don't want to add new interfaces!
                                continue;
                            }
                            e.insert(NetworkData {
                                inner: NetworkDataInner {
                                    ifi_ibytes: data.ifi_ibytes,
                                    old_ifi_ibytes: 0,
                                    ifi_obytes: data.ifi_obytes,
                                    old_ifi_obytes: 0,
                                    ifi_ipackets: data.ifi_ipackets,
                                    old_ifi_ipackets: 0,
                                    ifi_opackets: data.ifi_opackets,
                                    old_ifi_opackets: 0,
                                    ifi_ierrors: data.ifi_ierrors,
                                    old_ifi_ierrors: 0,
                                    ifi_oerrors: data.ifi_oerrors,
                                    old_ifi_oerrors: 0,
                                    updated: true,
                                    mac_addr: MacAddr::UNSPECIFIED,
                                    ip_networks: vec![],
                                    mtu,
                                },
                            });
                        }
                    }
                }
            }
        }
    }
}

struct InterfaceAddressIterator {
    /// Pointer to the current `ifaddrs` struct.
    ifap: *mut libc::ifaddrs,
    /// Pointer to the first element in linked list.
    buf: *mut libc::ifaddrs,
}

impl InterfaceAddressIterator {
    fn new() -> Option<Self> {
        let mut ifap = std::ptr::null_mut();
        if unsafe { retry_eintr!(libc::getifaddrs(&mut ifap)) } == 0 && !ifap.is_null() {
            Some(Self { ifap, buf: ifap })
        } else {
            None
        }
    }
}

impl Iterator for InterfaceAddressIterator {
    type Item = *mut libc::ifaddrs;

    fn next(&mut self) -> Option<Self::Item> {
        unsafe {
            while !self.ifap.is_null() {
                // advance the pointer until a MAC address is found
                // Safety: `ifap` is already checked as non-null in the loop condition.
                let ifap = self.ifap;
                let r_ifap = &*ifap;
                self.ifap = r_ifap.ifa_next;

                if r_ifap.ifa_addr.is_null()
                    || (*r_ifap.ifa_addr).sa_family as libc::c_int != libc::AF_LINK
                    || r_ifap.ifa_flags & libc::IFF_LOOPBACK as libc::c_uint != 0
                {
                    continue;
                }
                return Some(ifap);
            }
            None
        }
    }
}

impl Drop for InterfaceAddressIterator {
    fn drop(&mut self) {
        unsafe {
            libc::freeifaddrs(self.buf);
        }
    }
}

pub(crate) struct NetworkDataInner {
    /// Total number of bytes received over interface.
    ifi_ibytes: u64,
    old_ifi_ibytes: u64,
    /// Total number of bytes transmitted over interface.
    ifi_obytes: u64,
    old_ifi_obytes: u64,
    /// Total number of packets received.
    ifi_ipackets: u64,
    old_ifi_ipackets: u64,
    /// Total number of packets transmitted.
    ifi_opackets: u64,
    old_ifi_opackets: u64,
    /// Shows the total number of packets received with error. This includes
    /// too-long-frames errors, ring-buffer overflow errors, CRC errors,
    /// frame alignment errors, fifo overruns, and missed packets.
    ifi_ierrors: u64,
    old_ifi_ierrors: u64,
    /// similar to `ifi_ierrors`
    ifi_oerrors: u64,
    old_ifi_oerrors: u64,
    /// Whether or not the above data has been updated during refresh
    updated: bool,
    /// MAC address
    pub(crate) mac_addr: MacAddr,
    /// IP networks
    pub(crate) ip_networks: Vec<IpNetwork>,
    /// Interface Maximum Transfer Unit (MTU)
    mtu: u64,
}

impl NetworkDataInner {
    pub(crate) fn received(&self) -> u64 {
        self.ifi_ibytes.saturating_sub(self.old_ifi_ibytes)
    }

    pub(crate) fn total_received(&self) -> u64 {
        self.ifi_ibytes
    }

    pub(crate) fn transmitted(&self) -> u64 {
        self.ifi_obytes.saturating_sub(self.old_ifi_obytes)
    }

    pub(crate) fn total_transmitted(&self) -> u64 {
        self.ifi_obytes
    }

    pub(crate) fn packets_received(&self) -> u64 {
        self.ifi_ipackets.saturating_sub(self.old_ifi_ipackets)
    }

    pub(crate) fn total_packets_received(&self) -> u64 {
        self.ifi_ipackets
    }

    pub(crate) fn packets_transmitted(&self) -> u64 {
        self.ifi_opackets.saturating_sub(self.old_ifi_opackets)
    }

    pub(crate) fn total_packets_transmitted(&self) -> u64 {
        self.ifi_opackets
    }

    pub(crate) fn errors_on_received(&self) -> u64 {
        self.ifi_ierrors.saturating_sub(self.old_ifi_ierrors)
    }

    pub(crate) fn total_errors_on_received(&self) -> u64 {
        self.ifi_ierrors
    }

    pub(crate) fn errors_on_transmitted(&self) -> u64 {
        self.ifi_oerrors.saturating_sub(self.old_ifi_oerrors)
    }

    pub(crate) fn total_errors_on_transmitted(&self) -> u64 {
        self.ifi_oerrors
    }

    pub(crate) fn mac_address(&self) -> MacAddr {
        self.mac_addr
    }

    pub(crate) fn ip_networks(&self) -> &[IpNetwork] {
        &self.ip_networks
    }

    pub(crate) fn mtu(&self) -> u64 {
        self.mtu
    }
}
